// Dashboard.js 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 { CareerSuggestions } from './CareerSuggestions.js'; import PopoutPanel from './PopoutPanel.js'; import MilestoneTracker from './MilestoneTracker.js' import './Dashboard.css'; import Chatbot from "./Chatbot.js"; import { Bar } from 'react-chartjs-2'; import { fetchSchools } from '../utils/apiUtils.js'; ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); function Dashboard() { const location = useLocation(); const navigate = useNavigate(); const [careerSuggestions, setCareerSuggestions] = useState([]); const [careerDetails, setCareerDetails] = useState(null); const [riaSecScores, setRiaSecScores] = useState([]); const [selectedCareer, setSelectedCareer] = useState(null); const [schools, setSchools] = useState([]); const [salaryData, setSalaryData] = useState([]); const [economicProjections, setEconomicProjections] = useState(null); const [tuitionData, setTuitionData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [userState, setUserState] = useState(null); const [areaTitle, setAreaTitle] = useState(null); const [userZipcode, setUserZipcode] = useState(null); const [riaSecDescriptions, setRiaSecDescriptions] = useState([]); const [selectedJobZone, setSelectedJobZone] = useState(''); const [careersWithJobZone, setCareersWithJobZone] = useState([]); const [selectedFit, setSelectedFit] = useState(''); const [results, setResults] = useState([]); const [chatbotContext, setChatbotContext] = useState({}); const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [sessionHandled, setSessionHandled] = useState(false); const handleUnauthorized = () => { if (!sessionHandled) { setSessionHandled(true); setShowSessionExpiredModal(true); // Show session expired modal } }; // Function to handle the token check and fetch requests const authFetch = async (url, options = {}, onUnauthorized) => { const token = localStorage.getItem("token"); if (!token) { console.log("Token is missing, triggering session expired modal."); if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal return null; } const finalOptions = { ...options, headers: { ...(options.headers || {}), Authorization: `Bearer ${token}`, // Attach the token to the request }, }; try { const res = await fetch(url, finalOptions); // Log the response status for debugging console.log("Response Status:", res.status); if (res.status === 401 || res.status === 403) { console.log("Session expired, triggering session expired modal."); if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal return null; } return res; } catch (err) { console.error("Fetch error:", err); if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal return null; } }; // Fetch User Profile (with proper session handling) const fetchUserProfile = async () => { const res = await authFetch(`${apiUrl}/user-profile`); if (!res) return; if (res.ok) { const profileData = await res.json(); setUserState(profileData.state); setAreaTitle(profileData.area.trim() || ''); setUserZipcode(profileData.zipcode); } else { console.error('Failed to fetch user profile'); } }; const jobZoneLabels = { '1': 'Little or No Preparation', '2': 'Some Preparation Needed', '3': 'Medium Preparation Needed', '4': 'Considerable Preparation Needed', '5': 'Extensive Preparation Needed' }; const fitLabels = { 'Best': 'Best - Very Strong Match', 'Great': 'Great - Strong Match', 'Good': 'Good - Less Strong Match' }; const apiUrl = process.env.REACT_APP_API_URL || ''; useEffect(() => { const fetchUserProfile = async () => { const res = await authFetch(`${apiUrl}/user-profile`); if (!res) return; if (res.ok) { const profileData = await res.json(); setUserState(profileData.state); setAreaTitle(profileData.area.trim() || ''); setUserZipcode(profileData.zipcode); } else { console.error('Failed to fetch user profile'); } }; fetchUserProfile(); }, [apiUrl]); useEffect(() => { const fetchJobZones = async () => { if (careerSuggestions.length === 0) return; const socCodes = careerSuggestions.map((career) => career.code); try { const response = await axios.post(`${apiUrl}/job-zones`, { socCodes }); const jobZoneData = response.data; const updatedCareers = careerSuggestions.map((career) => ({ ...career, job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, })); setCareersWithJobZone(updatedCareers); } catch (error) { console.error('Error fetching job zone information:', error); } }; fetchJobZones(); }, [careerSuggestions, apiUrl]); const filteredCareers = useMemo(() => { return careersWithJobZone.filter((career) => { const jobZoneMatches = selectedJobZone ? career.job_zone !== null && career.job_zone !== undefined && typeof career.job_zone === 'number' && Number(career.job_zone) === Number(selectedJobZone) : true; const fitMatches = selectedFit ? career.fit === selectedFit : true; return jobZoneMatches && fitMatches; }); }, [careersWithJobZone, selectedJobZone, selectedFit]); const updateChatbotContext = (updatedData) => { setChatbotContext((prevContext) => { const mergedContext = { ...prevContext, ...Object.keys(updatedData).reduce((acc, key) => { if (updatedData[key] !== undefined && updatedData[key] !== null) { acc[key] = updatedData[key]; } return acc; }, {}), }; return mergedContext; }); }; const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]); 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]); 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]); useEffect(() => { if ( careerSuggestions.length > 0 && riaSecScores.length > 0 && userState !== null && areaTitle !== null && userZipcode !== null ) { const newChatbotContext = { careerSuggestions: [...careersWithJobZone], riaSecScores: [...riaSecScores], userState: userState || "", areaTitle: areaTitle || "", userZipcode: userZipcode || "", }; setChatbotContext(newChatbotContext); } else { } }, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]); const handleCareerClick = useCallback( async (career) => { const socCode = career.code; setSelectedCareer(career); setLoading(true); setError(null); setCareerDetails({}); setSchools([]); setSalaryData([]); setEconomicProjections({}); setTuitionData([]); if (!socCode) { console.error('SOC Code is missing'); setError('SOC Code is missing'); return; } 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(); let salaryResponse; try { salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } }); } catch (error) { salaryResponse = { data: {} }; } let economicResponse; try { economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`); } catch (error) { economicResponse = { data: {} }; } let tuitionResponse; try { tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } }); } catch (error) { tuitionResponse = { data: {} }; } 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 salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0 ? [ { percentile: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 }, { percentile: "25th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT25, 10) || 0 }, { percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 }, { percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 }, { percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 10) || 0 }, ] : []; const updatedCareerDetails = { ...career, jobDescription: description, tasks: tasks, economicProjections: economicResponse.data || {}, salaryData: salaryDataPoints, schools: schoolsWithDistance, tuitionData: tuitionResponse.data || [], }; setCareerDetails(updatedCareerDetails); updateChatbotContext({ careerDetails: updatedCareerDetails }); } catch (error) { console.error('Error processing career click:', error.message); setError('Failed to load data'); } finally { setLoading(false); } }, [userState, apiUrl, areaTitle, userZipcode] ); const chartData = { labels: riaSecScores.map((score) => score.area), datasets: [ { label: 'RIASEC Scores', data: riaSecScores.map((score) => score.score), backgroundColor: 'rgba(75, 192, 192, 0.2)', borderColor: 'rgba(75, 192, 192, 1)', borderWidth: 1, }, ], }; return (
{showSessionExpiredModal && (

Session Expired

Your session has expired or is invalid.

)}

RIASEC Scores

RIASEC Personality Descriptions

{riaSecDescriptions.length > 0 ? (
    {riaSecDescriptions.map((desc, index) => (
  • {riaSecScores[index]?.area}: {desc}
  • ))}
) : (

Loading descriptions...

)}
{memoizedPopoutPanel}
{careerSuggestions.length > 0 ? ( ) : (

Loading Chatbot...

)}

Career results and RIASEC scores are provided by O*Net, in conjunction with the Bureau of Labor Statistics, and the National Center for Education Statistics (NCES).

); } export default Dashboard;