diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 481b400..a7991f9 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -3,13 +3,21 @@ import axios from 'axios'; import React, { useMemo, useState, useCallback, useEffect } from 'react'; 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 PopoutPanel from './PopoutPanel.js'; import MilestoneTracker from './MilestoneTracker.js'; -import CareerSearch from './CareerSearch.js'; // <--- Import your new search -import Chatbot from "./Chatbot.js"; +import CareerSearch from './CareerSearch.js'; // <--- Import your new search +import Chatbot from './Chatbot.js'; import './Dashboard.css'; import { Bar } from 'react-chartjs-2'; @@ -51,12 +59,10 @@ function Dashboard() { const [sessionHandled, setSessionHandled] = useState(false); // ============= NEW State ============= - // Holds the full career object { title, soc_code, cip_code } typed in CareerSearch const [pendingCareerForModal, setPendingCareerForModal] = useState(null); // We'll treat "loading" as "loadingSuggestions" const loadingSuggestions = loading; - // We'll consider the popout panel visible if there's a selectedCareer const popoutVisible = !!selectedCareer; // ============= Auth & URL Setup ============= @@ -64,9 +70,9 @@ function Dashboard() { // AUTH fetch const authFetch = async (url, options = {}, onUnauthorized) => { - const token = localStorage.getItem("token"); + const token = localStorage.getItem('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(); return null; } @@ -79,21 +85,21 @@ function Dashboard() { }; try { 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) { - console.log("Session expired, triggering session expired modal."); + console.log('Session expired, triggering session expired modal.'); if (typeof onUnauthorized === 'function') onUnauthorized(); return null; } return res; } catch (err) { - console.error("Fetch error:", err); + console.error('Fetch error:', err); if (typeof onUnauthorized === 'function') onUnauthorized(); return null; } }; - // ============= User Profile Fetch ============= + // ============= Fetch user profile ============= const fetchUserProfile = async () => { const res = await authFetch(`${apiUrl}/user-profile`); if (!res) return; @@ -103,32 +109,93 @@ function Dashboard() { setUserState(profileData.state); setAreaTitle(profileData.area.trim() || ''); setUserZipcode(profileData.zipcode); + // Store entire userProfile if needed + setUserProfile(profileData); } else { 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(() => { fetchUserProfile(); }, [apiUrl]); // load once - // ============= jobZone & fit Setup ============= + // ============= jobZone & Fit Setup ============= const jobZoneLabels = { '1': 'Little or No Preparation', '2': 'Some Preparation Needed', '3': 'Medium Preparation Needed', '4': 'Considerable Preparation Needed', - '5': 'Extensive Preparation Needed' + '5': 'Extensive Preparation Needed', }; const fitLabels = { - 'Best': 'Best - Very Strong Match', - 'Great': 'Great - Strong Match', - 'Good': 'Good - Less Strong Match' + Best: 'Best - Very Strong Match', + Great: 'Great - 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(() => { const fetchJobZones = async () => { if (careerSuggestions.length === 0) return; @@ -148,7 +215,7 @@ function Dashboard() { fetchJobZones(); }, [careerSuggestions, apiUrl]); - // Filter careers by job zone, fit + // ============= Filter by job zone, fit ============= const filteredCareers = useMemo(() => { return careersWithJobZone.filter((career) => { const jobZoneMatches = selectedJobZone @@ -163,7 +230,7 @@ function Dashboard() { }); }, [careersWithJobZone, selectedJobZone, selectedFit]); - // Merge updated data into chatbot context + // ============= Merge data into chatbot context ============= const updateChatbotContext = (updatedData) => { setChatbotContext((prevContext) => { 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 ( - 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(() => { if ( careerSuggestions.length > 0 && @@ -240,15 +257,15 @@ function Dashboard() { const newChatbotContext = { careerSuggestions: [...careersWithJobZone], riaSecScores: [...riaSecScores], - userState: userState || "", - areaTitle: areaTitle || "", - userZipcode: userZipcode || "", + userState: userState || '', + areaTitle: areaTitle || '', + userZipcode: userZipcode || '', }; setChatbotContext(newChatbotContext); } }, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]); - // ============= handleCareerClick (for tile clicks) ============= + // ============= handleCareerClick ============= const handleCareerClick = useCallback( async (career) => { console.log('[handleCareerClick] career =>', career); @@ -284,7 +301,9 @@ function Dashboard() { // Salary let salaryResponse; 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) { salaryResponse = { data: {} }; } @@ -300,73 +319,77 @@ function Dashboard() { // Tuition let tuitionResponse; 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) { tuitionResponse = { data: {} }; } // Fetch schools const filteredSchools = await fetchSchools(cleanedCipCode, userState); - const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => { - const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`; - try { - const response = await axios.post(`${apiUrl}/maps/distance`, { - userZipcode, - destinations: schoolAddress, - }); - const { distance, duration } = response.data; - return { ...school, distance, duration }; - } catch (error) { - return { ...school, distance: 'N/A', duration: 'N/A' }; - } - })); + const schoolsWithDistance = await Promise.all( + filteredSchools.map(async (school) => { + const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`; + try { + const response = await axios.post(`${apiUrl}/maps/distance`, { + userZipcode, + destinations: schoolAddress, + }); + const { distance, duration } = response.data; + return { ...school, distance, duration }; + } catch (error) { + return { ...school, distance: 'N/A', duration: 'N/A' }; + } + }) + ); // 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 - }, - ] - : []; + 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, + }, + ] + : []; // Build final details const updatedCareerDetails = { ...career, jobDescription: description, tasks: tasks, - economicProjections: (economicResponse.data || {}), + economicProjections: economicResponse.data || {}, salaryData: salaryDataPoints, schools: schoolsWithDistance, - tuitionData: (tuitionResponse.data || []), + tuitionData: tuitionResponse.data || [], }; setCareerDetails(updatedCareerDetails); updateChatbotContext({ careerDetails: updatedCareerDetails }); - } catch (error) { console.error('Error processing career click:', error.message); setError('Failed to load data'); @@ -377,20 +400,20 @@ function Dashboard() { [userState, apiUrl, areaTitle, userZipcode, updateChatbotContext] ); - // ============= Letting typed careers open PopoutPanel ============= - // Called if the user picks a career in "CareerSearch" => { title, soc_code, cip_code } - const handleCareerFromSearch = useCallback((obj) => { - // Convert to shape used by handleCareerClick => { code, title, cipCode } - const adapted = { - code: obj.soc_code, - title: obj.title, - cipCode: obj.cip_code - }; - console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted); - handleCareerClick(adapted); - }, [handleCareerClick]); + // ============= 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] + ); - // If the user typed a career and clicked confirm useEffect(() => { if (pendingCareerForModal) { console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal); @@ -433,6 +456,38 @@ function Dashboard() { ); }; + // ============= Popout Panel Setup ============= + const memoizedPopoutPanel = useMemo(() => { + return ( + setSelectedCareer(null)} + loading={loading} + error={error} + userState={userState} + results={results} + updateChatbotContext={updateChatbotContext} + /> + ); + }, [ + selectedCareer, + careerDetails, + schools, + salaryData, + economicProjections, + tuitionData, + loading, + error, + userState, + results, + updateChatbotContext, + ]); + return (
{showSessionExpiredModal && ( @@ -441,16 +496,19 @@ function Dashboard() {

Session Expired

Your session has expired or is invalid.

-
diff --git a/user_profile.db b/user_profile.db index 6bbf939..b56813f 100644 Binary files a/user_profile.db and b/user_profile.db differ